﻿using System;
using System.ServiceModel;
using System.Threading;
using CommonUtils;
using Ensemble;

namespace SearchFrontEnd
{
    using BackEndServerList = ServerList<SearchBackendClient, ISearchBackend>;
    using FrontEndMessage = ServerMessage<MessageType>;

    /// <summary>
    /// Handles all intra-group communication among FE servers.
    /// </summary>
    class FEGroupCommunication
    {
        #region Members and Properties

        private static Connection conn = null;
        private static Member memb = null;
        private bool done = false;
        private const string GROUP_NAME = "FE Search Engine Servers";


        public static Connection Conn
        {
            get { return conn; }
            set { conn = value; }
        }

        public static Member Memb
        {
            get { return memb; }
            set { memb = value; }
        }

        #endregion

        /// <summary>
        /// Constructor. 
        /// Creates the member for the current server and join the FE
        /// server group.
        /// </summary>
        public FEGroupCommunication()
        {
            // Join the FE servers group
            Conn = new Connection();
            Conn.Connect();

            JoinOps jops = new JoinOps();
            jops.group_name = GROUP_NAME;

            // Create the endpoint
            Memb = new Member(Conn);
            Memb.Join(jops);
        }

        /// <summary>
        /// Leaves the group.
        /// </summary>
        public void Quit()
        {
            Memb.Leave();
            done = true;
        }

        /// <summary>
        /// Processes a message received by this member.
        /// </summary>
        /// <param name="bytes">Message bytes</param>
        private static void ProcessMessage(byte[] bytes)
        {
            FrontEndMessage msg = FrontEndMessage.Deserialize(bytes);
            if (bytes == null)
            {
                Console.WriteLine("Failed to de-serialize message.");
                return;
            }

            Print.DebugEmit(
                String.Format(
                    "Received message {0} of type {1}",
                    msg.MsgID,
                    msg.MessageType)
                );

            switch (msg.MessageType)
            {
                case MessageType.AddBackEndSrv:
                {
                    Uri serverUri = new Uri((String)msg.Params);
                    EndpointAddress addr = new EndpointAddress(serverUri);
                    BackEndServerList.AddServer(
                                       "NetTcpBinding_ISearchBackend",
                                       addr
                                       );
                    
                    Print.DebugEmit(
                                String.Format(
                                    "Added BE server {0}",
                                    addr
                                    )
                                );
                    break;
                }
                case MessageType.RemoveBackEndSrv:
                {
                    Uri serverUri = new Uri((String)msg.Params);
                    EndpointAddress addr = new EndpointAddress(serverUri);
                    BackEndServerList.RemoveServer(addr);
                    Print.DebugEmit(
                            String.Format(
                                "Removed BE server {0}",
                                addr
                                )
                            );
                    break;
                }
                case MessageType.InvalidateCacheEntries:
                {
                    string content = (string) msg.Params;
                    SearchFrontEnd.InvalidateStaleCacheEntries(content);
                    Print.DebugEmit(
                            String.Format(
                                "Invalidated cache entries for {0}",
                                content
                                )
                            );
                    break;
                }
            }
        }

        /// <summary>
        /// Generate a message notifying group members that some cache
        /// entries need to be invalidated as a results of an index operation
        /// handled by this member.
        /// If the current server is the only one in the group, no message 
        /// will be sent.
        /// </summary>
        /// <param name="content">The content of the indexed document</param>
        public static void NotifyCacheInvalidateNeeded(string content)
        {
            if (Memb.current_view.nmembers == 1)
            {
                // no other servers exist to receive the notification
                return;
            }

            // Create the message to multicast
            FrontEndMessage msg = new FrontEndMessage(
                                    MessageType.InvalidateCacheEntries,
                                    content
                                    );
            byte[] msgBytes = msg.Serialize();
            if (msgBytes == null)
            {
                // we should never get here.
                Console.WriteLine(
                        "An error occurred while attempting to serialize message."
                        );

                return;
            }

            // Send the created message to everyone.
            lock (Conn)
            {
                bool done = false;
                while (!done)
                {
                    switch (Memb.current_status)
                    {
                        case Member.Status.Blocked:
                        {
                            Thread.Sleep(100);
                            break;
                        }
                        case Member.Status.Normal:
                        {
                            Memb.Cast(msgBytes);
                            done = true;
                            break;
                        }
                        case Member.Status.Left:
                        {
                            goto case Member.Status.Leaving;
                        }
                        case Member.Status.Leaving:
                        {
                            // throwing an exception which will cause an error at
                            // the client side, forcing the client to retry the 
                            // index request by approaching another FE server.
                            throw new Exception("Server is shutting down.");
                        }
                        default:
                        {
                            // the only states left unhandled are "Pre" and
                            // "Joining", but according to the assumptions in 
                            // the exercise, they're not possible at this point.
                            break;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Generate a message notifying group members that some BE server is 
        /// down and can be removed from the BE servers' list.
        /// We do a best effort to send the notification, If for some reason
        /// it fails we give and let the other FE learn for themselves that
        /// the BE server is down.
        /// If the current server is the only one in the group, no message 
        /// will be sent.
        /// </summary>
        /// <param name="addr"></param>
        public static void NotifyBackEndServerIsDown(EndpointAddress addr)
        {
            if (Memb.current_view.nmembers == 1)
            {
                // no other servers exist to receive the notification
                return;
            }

            // Create the message to multicast
            FrontEndMessage msg = new FrontEndMessage(
                                    MessageType.RemoveBackEndSrv,
                                    addr.ToString()
                                    );
            byte[] msgBytes = msg.Serialize();
            if (msgBytes == null)
            {
                // we should never get here.
                Console.WriteLine(
                        "An error occured while attempting to serialize message."
                        );

                // do not fail because of this, other FE servers will find
                // out for themselves that the server is down in due time.
                return;
            }

            // Send the created message to everyone - best effort, if we can't
            // send the message immediately, we give up and let the others
            // find out for themselves.
            lock (Conn)
            {
                if (Memb.current_status == Member.Status.Normal)
                {
                    Memb.Cast(msgBytes);
                }
            }
        }

        /// <summary>
        /// Main loop - polls for messages for this member and handles them.
        /// The loop is terminated when the user presses the return key in
        /// the console.
        /// </summary>
        public void MainLoop()
        {
            while (!done)
            {
                // Read all waiting messages from Ensemble
                while (Conn.Poll())
                {
                    Message msg = Conn.Recv();
                    switch (msg.mtype)
                    {
                        case UpType.VIEW:
                        {
                            //received a view change notification
                            Print.PrintNewViewMsg(Memb, msg);
                            break;    
                        }
                        case UpType.CAST:
                        {
                            Print.DebugEmit("Received UpType.CAST");
                            if (msg.data != null)
                            {
                                ProcessMessage(msg.data);
                            }
                            break;   
                        }
                            
                        case UpType.SEND:
                        {
                            Print.DebugEmit("Received UpType.SEND");
                            if (msg.data != null)
                            {
                                ProcessMessage(msg.data);
                            }
                            break;  
                        }

                        case UpType.BLOCK:
                        {
                            Print.DebugEmit("Received UpType.BLOCK");
                            Print.ColorEmit("Changing views..", Print.Foreground.Blue);
                            Memb.BlockOk();
                            break;      
                        }

                        case UpType.EXIT:
                        {
                            Print.DebugEmit("Received UpType.EXIT");
                            break; 
                        }
                       
                    }
                    Console.Out.Flush();
                }

                //Console.WriteLine("sleeping for a little while");
                Thread.Sleep(100);
            }
        }
    }
}
